Skip to content

Fix data grid undo/redo and multi-row deletion bugs#4

Merged
datlechin merged 1 commit intomainfrom
feat/history
Dec 23, 2025
Merged

Fix data grid undo/redo and multi-row deletion bugs#4
datlechin merged 1 commit intomainfrom
feat/history

Conversation

@datlechin
Copy link
Copy Markdown
Collaborator

  • Fix undo insert row then save error by adding safety checks in generateSQL()
  • Fix multi-row delete only deleting last row by implementing undoBatchRowInsertion()
  • Fix redo after delete causing 'Could not generate SQL' error
  • Fix Cmd+Z not restoring deleted inserted rows by adding batchRowInsertion undo action
  • Add proper undo/redo support for batch operations
  • Clean up verbose debug logging, keep only warnings

- Fix undo insert row then save error by adding safety checks in generateSQL()
- Fix multi-row delete only deleting last row by implementing undoBatchRowInsertion()
- Fix redo after delete causing 'Could not generate SQL' error
- Fix Cmd+Z not restoring deleted inserted rows by adding batchRowInsertion undo action
- Add proper undo/redo support for batch operations
- Clean up verbose debug logging, keep only warnings
Copilot AI review requested due to automatic review settings December 23, 2025 06:13
@datlechin datlechin merged commit 151a673 into main Dec 23, 2025
2 checks passed
@datlechin datlechin deleted the feat/history branch December 23, 2025 06:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes several critical bugs related to data grid undo/redo operations and multi-row deletion. The changes add proper batch operation support for deleting multiple inserted rows simultaneously, implement safety checks to prevent SQL generation errors, and clean up verbose debug logging.

Key changes:

  • Added batchRowInsertion undo action and undoBatchRowInsertion() method to handle deleting multiple inserted rows as a single atomic operation
  • Implemented safety checks in generateSQL() to verify rows are still in the correct state before generating SQL statements
  • Refactored deleteSelectedRows() to separate inserted vs existing row handling and properly manage batch deletions
  • Added rebuildTableQuery() utility to ensure queries reflect current filter/sort state when refreshing

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

File Description
SidebarView.swift Removed debug print statements for pendingDeletes operations
MainContentView.swift Refactored multi-row deletion logic, added undo/redo handlers for batch row insertion, improved refresh flow, removed debug logging
DataChange.swift Added batch row insertion undo action, implemented undoBatchRowInsertion(), added safety checks in generateSQL(), enhanced undo/redo logic
project.pbxproj Bumped version from 0.1.3 to 0.1.4 and build number from 3 to 4

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +397 to +416
// Collect row values BEFORE removing changes (for undo/redo)
var rowValues: [[String?]] = []
for rowIndex in validRows {
if let insertChange = changes.first(where: { $0.rowIndex == rowIndex && $0.type == .insert }) {
let values = insertChange.cellChanges.sorted { $0.columnIndex < $1.columnIndex }
.map { $0.newValue }
rowValues.append(values)
} else {
rowValues.append(Array(repeating: nil, count: columns.count))
}
}

// Remove all INSERT changes for these rows
for rowIndex in validRows {
changes.removeAll { $0.rowIndex == rowIndex && $0.type == .insert }
insertedRowIndices.remove(rowIndex)
}

// Push undo action so user can undo this deletion
pushUndo(.batchRowInsertion(rowIndices: validRows, rowValues: rowValues))
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential index mismatch bug between rowIndices and rowValues arrays. The code collects rowValues by iterating through validRows (filtered from rowIndices), but stores the undo action with validRows as rowIndices. If some rows are filtered out as invalid, the arrays will have different lengths, causing the indices to be mismatched when accessing rowValues[index] in the undo handler at line 1581 in MainContentView.swift. The arrays should always be in sync with the same length and order, either by using validRows consistently or by collecting rowValues only for valid rows and storing them in the same order.

Copilot uses AI. Check for mistakes.
Comment on lines +1580 to +1585
// Process in reverse order (ascending) to maintain correct indices
for (index, rowIndex) in rowIndices.enumerated().reversed() {
guard index < rowValues.count else { continue }
guard rowIndex <= tabManager.tabs[tabIndex].resultRows.count else { continue }

let values = rowValues[index]
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The undo logic for batchRowInsertion has an index mismatch. The code uses rowIndices.enumerated().reversed() which pairs each index with its position in the array (0, 1, 2...), but then uses that enumeration index to access rowValues[index]. This means if rowIndices is [5, 3, 1], the enumeration will be [(2, 5), (1, 3), (0, 1)] when reversed, and you'll access rowValues[2], rowValues[1], rowValues[0]. However, rowValues was built by iterating through validRows in forward order, so rowValues[0] corresponds to validRows[0], not necessarily rowIndices[0]. The correct approach is to iterate through the arrays together using zip or indices: for (rowIndex, values) in zip(rowIndices, rowValues).reversed()

Suggested change
// Process in reverse order (ascending) to maintain correct indices
for (index, rowIndex) in rowIndices.enumerated().reversed() {
guard index < rowValues.count else { continue }
guard rowIndex <= tabManager.tabs[tabIndex].resultRows.count else { continue }
let values = rowValues[index]
// Process in reverse order to maintain correct indices when inserting
for (rowIndex, values) in zip(rowIndices, rowValues).reversed() {
guard rowIndex <= tabManager.tabs[tabIndex].resultRows.count else { continue }

Copilot uses AI. Check for mistakes.
Comment on lines +1044 to +1045
let rowsDeleted = insertedRowsToDelete.count
let adjustedMaxRow = maxSelectedRow - rowsDeleted
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for calculating adjustedMaxRow is incorrect. It subtracts the total count of deleted inserted rows from maxSelectedRow, but it should only subtract the number of deleted inserted rows that are BELOW maxSelectedRow, not all of them. If you delete rows [1, 5, 7] and maxSelectedRow is 5, the adjusted position should be 5 - 1 = 4 (only row 1 is below it), not 5 - 3 = 2. The correct calculation should be: insertedRowsToDelete.filter { $0 < maxSelectedRow }.count

Suggested change
let rowsDeleted = insertedRowsToDelete.count
let adjustedMaxRow = maxSelectedRow - rowsDeleted
let rowsDeletedBelowMax = insertedRowsToDelete.filter { $0 < maxSelectedRow }.count
let adjustedMaxRow = maxSelectedRow - rowsDeletedBelowMax

Copilot uses AI. Check for mistakes.
// Redo the deletion of inserted rows - remove them again
// This is called when user: delete inserted rows -> undo -> redo
// We need to remove the rows from changes and insertedRowIndices again
for rowIndex in rowIndices {
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The redo logic for batchRowInsertion doesn't handle index shifting. When removing multiple inserted rows from changes and insertedRowIndices, the code iterates through rowIndices without adjusting for the fact that removing rows changes the indices of subsequent rows. This should either process rowIndices in descending order to avoid index shifting issues, or should apply the same index shifting logic used in undoBatchRowInsertion lines 419-435. Without this, remaining inserted rows will have incorrect indices after the redo operation.

Suggested change
for rowIndex in rowIndices {
// Process in descending index order to avoid index-shifting issues
let sortedRowIndices = rowIndices.sorted(by: >)
for rowIndex in sortedRowIndices {

Copilot uses AI. Check for mistakes.
Comment on lines +544 to +564
case .batchRowInsertion(let rowIndices, let rowValues):
// Undo the deletion of inserted rows - restore them as INSERT changes
// Process in reverse order (ascending) to maintain correct indices when re-inserting
for (index, rowIndex) in rowIndices.enumerated().reversed() {
guard index < rowValues.count else { continue }
let values = rowValues[index]

// Re-create INSERT change
let cellChanges = values.enumerated().map { colIndex, value in
CellChange(
rowIndex: rowIndex,
columnIndex: colIndex,
columnName: columns[safe: colIndex] ?? "",
oldValue: nil,
newValue: value
)
}
let rowChange = RowChange(rowIndex: rowIndex, type: .insert, cellChanges: cellChanges)
changes.append(rowChange)
insertedRowIndices.insert(rowIndex)
}
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The undo logic for batchRowInsertion doesn't handle index shifting when restoring multiple inserted rows. The code processes rows in reverse order (ascending indices) which is correct for insertion, but it doesn't shift the indices of other inserted rows and changes that come after each restored row. When a row is restored, all subsequent inserted row indices should be incremented. Without this adjustment, the indices in insertedRowIndices and changes array will be incorrect, potentially causing issues with future operations.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants